package me.tonywang.quartz;

import org.quartz.*;
import org.quartz.impl.JobDetailImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.JobMethodInvocationFailedException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.util.Assert;
import org.springframework.util.MethodInvoker;
import org.springframework.util.StringUtils;

import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;

import static org.quartz.CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING;

/**
 * @className: CronTaskFactoryBean
 * @description:
 * @author: WANGHUI
 * @createDate: 2018/5/15 10:35
 * @version: 1.0
 * @see JobDetailFactoryBean
 * @see CronTriggerFactoryBean
 * @see org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean.MethodInvokingJob
 */

public class CronTaskFactoryBean extends CronTriggerFactoryBean {

    private String targetBeanName;
    private String targetMethod;
    private Object requestObj;

    /**
     * triggerName =jobName 两者保持一致
     */
    private String name;

    /**
     * triggerGroupName =jobGroupName 两者保持一致
     */
    private String group = Scheduler.DEFAULT_GROUP;

    private boolean concurrent = false;

    private boolean durability = false;

    private boolean requestsRecovery = false;

    @Nullable
    private String description;


    public void setTargetBeanName(String targetBeanName) {
        this.targetBeanName = targetBeanName;
    }

    public void setTargetMethod(String targetMethod) {
        this.targetMethod = targetMethod;
    }

    public void setRequestObj(Object requestObj) {
        this.requestObj = requestObj;
    }

    /**
     * <p>
     * Set whether or not the <code>Job</code> should remain stored after it
     * is orphaned (no <code>{@link Trigger}s</code> point to it).
     * </p>
     *
     * <p>
     * If not explicitly set, the default value is <code>false</code>.
     * </p>
     */
    public void setDurability(boolean durability) {
        this.durability = durability;
    }

    /**
     * <p>
     * Set whether or not the the <code>Scheduler</code> should re-execute
     * the <code>Job</code> if a 'recovery' or 'fail-over' situation is
     * encountered.
     * </p>
     *
     * <p>
     * If not explicitly set, the default value is <code>false</code>.
     * </p>
     *
     * @see JobExecutionContext#isRecovering()
     */
    public void setRequestsRecovery(boolean requestsRecovery) {
        this.requestsRecovery = requestsRecovery;
    }

    @Override
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * triggerName =jobName
     */
    @Override
    public void setName(String name) {
        super.setName(name);
        this.name = name;
    }

    /**
     * triggerGroupName =jobGroupName
     */
    @Override
    public void setGroup(String group) {
        super.setGroup(group);
        this.group = group;
    }

    public void setConcurrent(boolean concurrent) {
        this.concurrent = concurrent;
    }


    /**
     * @return
     * @see org.springframework.scheduling.quartz.JobDetailFactoryBean
     * @see org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean.MethodInvokingJob
     * MethodInvokingJob中的methodInvoker不可在JobDataMap中序列化，需要改写；
     */
    public JobDetail getJobDetail() {

        Class jobClass = (this.concurrent ? MethodInvokingJob.class : StatefulMethodInvokingJob.class);

        JobDataMap data = new JobDataMap();
        data.put("targetBeanName", targetBeanName);
        data.put("targetMethod", targetMethod);
        data.put("requestObj", requestObj);

        JobDetailImpl jdi = new JobDetailImpl();
        jdi.setName(this.name != null ? this.name : toString());
        jdi.setGroup(this.group);
        jdi.setJobClass(jobClass);
        jdi.setJobDataMap(data);
        jdi.setDurability(this.durability);
        jdi.setRequestsRecovery(this.requestsRecovery);
        jdi.setDescription(this.description);
        return jdi;
    }

    public CronTrigger getCronTrigger() {
        return getObject();
    }


    @Override
    public CronTrigger getObject() {
        setJobDetail(getJobDetail());
        try {
            super.setMisfireInstruction(MISFIRE_INSTRUCTION_DO_NOTHING);
            afterPropertiesSet();
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
        return super.getObject();
    }


    /**
     * Quartz Job implementation that invokes a specified method.
     * <p>
     * 1.SchedulerFactoryBean将applicationContext放入SchedulerContext
     * 2.QuartzJobBean execute方法 从getMergedJobDataMap 和 SchedulerContext 中取出对应属性
     * 3.QuartzJobBean execute方法 利用BeanWrapper 复制内容到当前QuartzJobBean的子类
     *
     * @see org.springframework.scheduling.quartz.SchedulerFactoryBean
     * @see QuartzJobBean
     */
    public static class MethodInvokingJob extends QuartzJobBean {

        private String targetBeanName;
        private String targetMethod;
        private String requestObj;

        private ApplicationContext ctx;

        MethodInvoker invoker = new MethodInvoker();

        @Override
        protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
            try {
                prepareMethodInvoker() ;
                //
                context.setResult(invoker.invoke());
            } catch (InvocationTargetException ex) {
                if (ex.getTargetException() instanceof JobExecutionException) {
                    // -> JobExecutionException, to be logged at info level by Quartz
                    throw (JobExecutionException) ex.getTargetException();
                } else {
                    // -> "unhandled exception", to be logged at error level by Quartz
                    throw new JobMethodInvocationFailedException(invoker, ex.getTargetException());
                }
            } catch (Exception ex) {
                // -> "unhandled exception", to be logged at error level by Quartz
                throw new JobMethodInvocationFailedException(invoker, ex);
            }

        }

        /**
         * 1.  从  SchedulerContext 中取出applicationContextSchedulerContextKey对应的applicationContext
         * 2.  利用BeanWrapper 执行setApplicationContext 方法
         *
         * @param applicationContext
         * @see QuartzJobBean
         */
        public void setApplicationContext(ApplicationContext applicationContext) {
            this.ctx = applicationContext;
        }

        /**
         * 1.  从   mergedJobDataMap 中取出targetBeanName对应的字符串
         * 2.  利用BeanWrapper 执行 setTargetBeanName 方法
         *
         * @param targetBeanName
         * @see QuartzJobBean  execute方法
         */
        public void setTargetBeanName(String targetBeanName) {
            this.targetBeanName = targetBeanName;
        }

        /**
         * 1.  从   mergedJobDataMap 中取出targetBeanName对应的字符串
         * 2.  利用BeanWrapper 执行 setTargetMethod 方法
         *
         * @param targetMethod
         * @see QuartzJobBean execute方法
         */
        public void setTargetMethod(String targetMethod) {
            this.targetMethod = targetMethod;
        }

        /**
         * 1.  从  mergedJobDataMap 中取出requestObj对应的字符串
         * 2.  利用BeanWrapper 执行 setRequestObj 方法
         *
         * @param requestObj
         * @see QuartzJobBean execute方法
         */
        public void setRequestObj(String requestObj) {
            this.requestObj = requestObj;
        }

        private void prepareMethodInvoker() throws ClassNotFoundException, NoSuchMethodException {
            Assert.state(ctx != null, "BeanFactory must be set when using 'targetBeanName'");
            invoker.setTargetClass(this.ctx.getType(targetBeanName));
            invoker.setTargetObject(ctx.getBean(targetBeanName));
            invoker.setTargetMethod(targetMethod);
            if (StringUtils.hasText(requestObj))
                invoker.setArguments(requestObj);
            invoker.prepare();
        }

    }

    /**
     * Extension of the MethodInvokingJob, implementing the StatefulJob interface.
     * Quartz checks whether or not jobs are stateful and if so,
     * won't let jobs interfere with each other.
     *
     * @DisallowConcurrentExecution 将该注解加到job类上，告诉Quartz不要并发地执行同一个job定义（这里指特定的job类）的多个实例。
     * 请注意这里的用词。也就是说不同job的定义的实例存在并发
     */
    @PersistJobDataAfterExecution
    @DisallowConcurrentExecution
    public static class StatefulMethodInvokingJob extends MethodInvokingJob {

        // No implementation, just an addition of the tag interface StatefulJob
        // in order to allow stateful method invoking jobs.
    }
}
